/**
 * @fileOverview The "clipboard" plugin.
 */
(function() {
   'use strict';
   // Register the plugin.
   VED.plugins.add( 'clipboard', {
      requires: 'dialog',
      lang: 'en,ru', // %REMOVE_LINE_CORE%
      icons: 'copy,copy-rtl,cut,cut-rtl,paste,paste-rtl', // %REMOVE_LINE_CORE%
      hidpi: true, // %REMOVE_LINE_CORE%
      init: function( editor ) {
         var textificationFilter;

         initClipboard( editor );

         // VED.dialog.add( 'paste', VED.getUrl( this.path + 'dialogs/paste.js' ) );

         editor.on( 'paste', function( evt ) {
            var data = evt.data.dataValue,
               blockElements = VED.dtd.$block;

            // Filter webkit garbage.
            if ( data.indexOf( 'Apple-' ) > -1 ) {
               // Replace special webkit's &nbsp; with simple space, because webkit
               // produces them even for normal spaces.
               data = data.replace( /<span class="Apple-converted-space">&nbsp;<\/span>/gi, ' ' );

               // Strip <span> around white-spaces when not in forced 'html' content type.
               // This spans are created only when pasting plain text into Webkit,
               // but for safety reasons remove them always.
               if ( evt.data.type != 'html' )
                  data = data.replace( /<span class="Apple-tab-span"[^>]*>([^<]*)<\/span>/gi, function( all, spaces ) {
                  // Replace tabs with 4 spaces like Fx does.
                  return spaces.replace( /\t/g, '&nbsp;&nbsp; &nbsp;' );
               });

               // This br is produced only when copying & pasting HTML content.
               if ( data.indexOf( '<br class="Apple-interchange-newline">' ) > -1 ) {
                  evt.data.startsWithEOL = 1;
                  evt.data.preSniffing = 'html'; // Mark as not text.
                  data = data.replace( /<br class="Apple-interchange-newline">/, '' );
               }

               // Remove all other classes.
               data = data.replace( /(<[^>]+) class="Apple-[^"]*"/gi, '$1' );
            }

            // Strip editable that was copied from inside. (#9534)
            if ( data.match( /^<[^<]+ved_(editable|contents)/i ) ) {
               var tmp,
                  editable_wrapper,
                  wrapper = new VED.dom.element( 'div' );

               wrapper.setHtml( data );
               // Verify for sure and check for nested editor UI parts. (#9675)
               while ( wrapper.getChildCount() == 1 &&
                     ( tmp = wrapper.getFirst() ) &&
                     tmp.type == VED.NODE_ELEMENT &&   // Make sure first-child is element.
                     ( tmp.hasClass( 'ved_editable' ) || tmp.hasClass( 'ved_contents' ) ) ) {
                  wrapper = editable_wrapper = tmp;
               }

               // If editable wrapper was found strip it and bogus <br> (added on FF).
               if ( editable_wrapper )
                  data = editable_wrapper.getHtml().replace( /<br>$/i, '' );
            }

            if ( VED.env.ie ) {
               // &nbsp; <p> -> <p> (br.ved-pasted-remove will be removed later)
               data = data.replace( /^&nbsp;(?: |\r\n)?<(\w+)/g, function( match, elementName ) {
                  if ( elementName.toLowerCase() in blockElements ) {
                     evt.data.preSniffing = 'html'; // Mark as not a text.
                     return '<' + elementName;
                  }
                  return match;
               });
            } else if ( VED.env.webkit ) {
               // </p><div><br></div> -> </p><br>
               // We don't mark br, because this situation can happen for htmlified text too.
               data = data.replace( /<\/(\w+)><div><br><\/div>$/, function( match, elementName ) {
                  if ( elementName in blockElements ) {
                     evt.data.endsWithEOL = 1;
                     return '</' + elementName + '>';
                  }
                  return match;
               });
            } else if ( VED.env.gecko ) {
               // Firefox adds bogus <br> when user pasted text followed by space(s).
               data = data.replace( /(\s)<br>$/, '$1' );
            }

            evt.data.dataValue = data;
         }, null, null, 3 );

         editor.on( 'paste', function( evt ) {
            var dataObj = evt.data,
               type = dataObj.type,
               data = dataObj.dataValue,
               trueType,
               // Default is 'html'.
               defaultType = editor.config.clipboard_defaultContentType || 'html';

            // If forced type is 'html' we don't need to know true data type.
            if ( type == 'html' || dataObj.preSniffing == 'html' )
               trueType = 'html';
            else
               trueType = recogniseContentType( data );

            // Unify text markup.
            if ( trueType == 'htmlifiedtext' )
               data = htmlifiedTextHtmlification( editor.config, data );
            // Strip presentional markup & unify text markup.
            else if ( type == 'text' && trueType == 'html' ) {
               // Init filter only if needed and cache it.
               data = htmlTextification( editor.config, data, textificationFilter || ( textificationFilter = getTextificationFilter( editor ) ) );
            }

            if ( dataObj.startsWithEOL )
               data = '<br data-ved-eol="1">' + data;
            if ( dataObj.endsWithEOL )
               data += '<br data-ved-eol="1">';

            if ( type == 'auto' )
               type = ( trueType == 'html' || defaultType == 'html' ) ? 'html' : 'text';

            dataObj.type = type;
            dataObj.dataValue = data;
            delete dataObj.preSniffing;
            delete dataObj.startsWithEOL;
            delete dataObj.endsWithEOL;
         }, null, null, 6 );

         // Inserts processed data into the editor at the end of the
         // events chain.
         editor.on( 'paste', function( evt ) {
            var data = evt.data;

            editor.insertHtml( data.dataValue, data.type );

            // Deferr 'afterPaste' so all other listeners for 'paste' will be fired first.
            setTimeout( function() {
               editor.fire( 'afterPaste' );
            }, 0 );
         }, null, null, 1000 );

         editor.on( 'pasteDialog', function( evt ) {
            // TODO it's possible that this setTimeout is not needed any more,
            // because of changes introduced in the same commit as this comment.
            // Editor.getClipboardData adds listner to the dialog's events which are
            // fired after a while (not like 'showDialog').
            setTimeout( function() {
               // Open default paste dialog.
               editor.openDialog( 'paste', evt.data );
            }, 0 );
         });
      }
   });

   function initClipboard( editor ) {
      var preventBeforePasteEvent = 0,
         preventPasteEvent = 0,
         inReadOnly = 0,
         // Safari doesn't like 'beforepaste' event - it sometimes doesn't
         // properly handles ctrl+c. Probably some race-condition between events.
         // Chrome and Firefox works well with both events, so better to use 'paste'
         // which will handle pasting from e.g. browsers' menu bars.
         // IE7/8 doesn't like 'paste' event for which it's throwing random errors.
         mainPasteEvent = VED.env.ie ? 'beforepaste' : 'paste';

      addListeners();
      addButtonsCommands();

      editor.getClipboardData = function( options, callback ) {
         var beforePasteNotCanceled = false,
            dataType = 'auto',
            dialogCommited = false;

         // Options are optional - args shift.
         if ( !callback ) {
            callback = options;
            options = null;
         }

         // Listen with maximum priority to handle content before everyone else.
         // This callback will handle paste event that will be fired if direct
         // access to the clipboard succeed in IE.
         editor.on( 'paste', onPaste, null, null, 0 );

         // Listen at the end of listeners chain to see if event wasn't canceled
         // and to retrieve modified data.type.
         editor.on( 'beforePaste', onBeforePaste, null, null, 1000 );

         // getClipboardDataDirectly() will fire 'beforePaste' synchronously, so we can
         // check if it was canceled and if any listener modified data.type.

         // If command didn't succeed (only IE allows to access clipboard and only if
         // user agrees) open and handle paste dialog.
         if ( getClipboardDataDirectly() === false ) {
            // Direct access to the clipboard wasn't successful so remove listener.
            editor.removeListener( 'paste', onPaste );

            // If beforePaste was canceled do not open dialog.
            // Add listeners only if dialog really opened. 'pasteDialog' can be canceled.
            if ( beforePasteNotCanceled && editor.fire( 'pasteDialog', onDialogOpen ) ) {
               editor.on( 'pasteDialogCommit', onDialogCommit );

               // 'dialogHide' will be fired after 'pasteDialogCommit'.
               editor.on( 'dialogHide', function( evt ) {
                  evt.removeListener();
                  evt.data.removeListener( 'pasteDialogCommit', onDialogCommit );

                  // Because Opera has to wait a while in pasteDialog we have to wait here.
                  setTimeout( function() {
                     // Notify even if user canceled dialog (clicked 'cancel', ESC, etc).
                     if ( !dialogCommited )
                        callback( null );
                  }, 10 );
               });
            } else
               callback( null );
         }

         function onPaste( evt ) {
            evt.removeListener();
            evt.cancel();
            callback( evt.data );
         }

         function onBeforePaste( evt ) {
            evt.removeListener();
            beforePasteNotCanceled = true;
            dataType = evt.data.type;
         }

         function onDialogCommit( evt ) {
            evt.removeListener();
            // Cancel pasteDialogCommit so paste dialog won't automatically fire
            // 'paste' evt by itself.
            evt.cancel();
            dialogCommited = true;
            callback( { type: dataType, dataValue: evt.data } );
         }

         function onDialogOpen() {
            this.customTitle = ( options && options.title );
         }
      };

      function addButtonsCommands() {
         addButtonCommand( 'Cut', 'cut', createCutCopyCmd( 'cut' ), 10, 1 );
         addButtonCommand( 'Copy', 'copy', createCutCopyCmd( 'copy' ), 20, 4 );
         addButtonCommand( 'Paste', 'paste', createPasteCmd(), 30, 8 );

         function addButtonCommand( buttonName, commandName, command, toolbarOrder, ctxMenuOrder ) {
            var lang = editor.lang.clipboard[ commandName ];

            editor.addCommand( commandName, command );
            editor.ui.addButton && editor.ui.addButton( buttonName, {
               label: lang,
               command: commandName,
               toolbar: 'clipboard,' + toolbarOrder
            });

            // If the "menu" plugin is loaded, register the menu item.
            if ( editor.addMenuItems ) {
               editor.addMenuItem( commandName, {
                  label: lang,
                  command: commandName,
                  group: 'clipboard',
                  order: ctxMenuOrder
               });
            }
         }
      }

      function addListeners() {
         editor.on( 'key', onKey );
         editor.on( 'contentDom', addListenersToEditable );

         // For improved performance, we're checking the readOnly state on selectionChange instead of hooking a key event for that.
         editor.on( 'selectionChange', function( evt ) {
            inReadOnly = evt.data.selection.getRanges()[ 0 ].checkReadOnly();
            setToolbarStates();
         });

         // If the "contextmenu" plugin is loaded, register the listeners.
         if ( editor.contextMenu ) {
            editor.contextMenu.addListener( function( element, selection ) {
               inReadOnly = selection.getRanges()[ 0 ].checkReadOnly();
               return {
                  cut: stateFromNamedCommand( 'Cut' ),
                  copy: stateFromNamedCommand( 'Copy' ),
                  paste: stateFromNamedCommand( 'Paste' )
               };
            });
         }
      }

      // Add events listeners to editable.
      function addListenersToEditable() {
         var editable = editor.editable();

         // We'll be catching all pasted content in one line, regardless of whether
         // it's introduced by a document command execution (e.g. toolbar buttons) or
         // user paste behaviors (e.g. CTRL+V).
         editable.on( mainPasteEvent, function( evt ) {
            if ( VED.env.ie && preventBeforePasteEvent )
               return;

            // If you've just asked yourself why preventPasteEventNow() is not here, but
            // in listener for CTRL+V and exec method of 'paste' command
            // you've asked the same question we did.
            //
            // THE ANSWER:
            //
            // First thing to notice - this answer makes sense only for IE,
            // because other browsers don't listen for 'paste' event.
            //
            // What would happen if we move preventPasteEventNow() here?
            // For:
            // * CTRL+V - IE fires 'beforepaste', so we prevent 'paste' and pasteDataFromClipboard(). OK.
            // * editor.execCommand( 'paste' ) - we fire 'beforepaste', so we prevent
            //      'paste' and pasteDataFromClipboard() and doc.execCommand( 'Paste' ). OK.
            // * native context menu - IE fires 'beforepaste', so we prevent 'paste', but unfortunately
            //      on IE we fail with pasteDataFromClipboard() here, because of... we don't know why, but
            //      we just fail, so... we paste nothing. FAIL.
            // * native menu bar - the same as for native context menu.
            //
            // But don't you know any way to distinguish first two cases from last two?
            // Only one - special flag set in CTRL+V handler and exec method of 'paste'
            // command. And that's what we did using preventPasteEventNow().

            pasteDataFromClipboard( evt );
         });

         // It's not possible to clearly handle all four paste methods (ctrl+v, native menu bar
         // native context menu, editor's command) in one 'paste/beforepaste' event in IE.
         //
         // For ctrl+v & editor's command it's easy to handle pasting in 'beforepaste' listener,
         // so we do this. For another two methods it's better to use 'paste' event.
         //
         // 'paste' is always being fired after 'beforepaste' (except of weird one on opening native
         // context menu), so for two methods handled in 'beforepaste' we're canceling 'paste'
         // using preventPasteEvent state.
         //
         // 'paste' event in IE is being fired before getClipboardDataByPastebin executes its callback.
         //
         // QUESTION: Why didn't you handle all 4 paste methods in handler for 'paste'?
         //      Wouldn't this just be simpler?
         // ANSWER: Then we would have to evt.data.preventDefault() only for native
         //      context menu and menu bar pastes. The same with execIECommand().
         //      That would force us to mark CTRL+V and editor's paste command with
         //      special flag, other than preventPasteEvent. But we still would have to
         //      have preventPasteEvent for the second event fired by execIECommand.
         //      Code would be longer and not cleaner.
         VED.env.ie && editable.on( 'paste', function( evt ) {
            if ( preventPasteEvent )
               return;
            // Cancel next 'paste' event fired by execIECommand( 'paste' )
            // at the end of this callback.
            preventPasteEventNow();

            // Prevent native paste.
            evt.data.preventDefault();

            pasteDataFromClipboard( evt );

            // Force IE to paste content into pastebin so pasteDataFromClipboard will work.
            if ( !execIECommand( 'paste' ) )
               editor.openDialog( 'paste' );
         });

         // [IE] Dismiss the (wrong) 'beforepaste' event fired on context/toolbar menu open. (#7953)
         if ( VED.env.ie ) {
            editable.on( 'contextmenu', preventBeforePasteEventNow, null, null, 0 );

            editable.on( 'beforepaste', function( evt ) {
               if ( evt.data && !evt.data.$.ctrlKey )
                  preventBeforePasteEventNow();
            }, null, null, 0 );

         }

         editable.on( 'beforecut', function() {
            !preventBeforePasteEvent && fixCut( editor );
         });

         var mouseupTimeout;

         // Use editor.document instead of editable in non-IEs for observing mouseup
         // since editable won't fire the event if selection process started within
         // iframe and ended out of the editor (#9851).
         editable.attachListener( VED.env.ie ? editable : editor.document.getDocumentElement(), 'mouseup', function() {
            mouseupTimeout = setTimeout( function() {
               setToolbarStates();
            }, 0 );
         });

         // Make sure that deferred mouseup callback isn't executed after editor instance
         // had been destroyed. This may happen when editor.destroy() is called in parallel
         // with mouseup event (i.e. a button with onclick callback) (#10219).
         editor.on( 'destroy', function() {
            clearTimeout( mouseupTimeout );
         });

         editable.on( 'keyup', setToolbarStates );
      }

      // Create object representing Cut or Copy commands.
      function createCutCopyCmd( type ) {
         return {
            type: type,
            canUndo: type == 'cut', // We can't undo copy to clipboard.
            startDisabled: true,
            exec: function( data ) {
               // Attempts to execute the Cut and Copy operations.
               function tryToCutCopy( type ) {
                  if ( VED.env.ie )
                     return execIECommand( type );

                  // non-IEs part
                  try {
                     // Other browsers throw an error if the command is disabled.
                     return editor.document.$.execCommand( type, false, null );
                  } catch ( e ) {
                     return false;
                  }
               }

               this.type == 'cut' && fixCut();

               var success = tryToCutCopy( this.type );

               if ( !success )
                  alert( editor.lang.clipboard[ this.type + 'Error' ] ); // Show cutError or copyError.

               return success;
            }
         };
      }

      function createPasteCmd() {
         return {
            // Snapshots are done manually by editable.insertXXX methods.
            canUndo: false,
            async: true,

            exec: function( editor, data ) {
               var fire = function( data, withBeforePaste ) {
                     data && firePasteEvents( data.type, data.dataValue, !!withBeforePaste );

                     editor.fire( 'afterCommandExec', {
                        name: 'paste',
                        command: cmd,
                        returnValue: !!data
                     });
                  },
                  cmd = this;

               // Check data precisely - don't open dialog on empty string.
               if ( typeof data == 'string' )
                  fire( { type: 'auto', dataValue: data }, 1 );
               else
                  editor.getClipboardData( fire );
            }
         };
      }

      function preventPasteEventNow() {
         preventPasteEvent = 1;
         // For safety reason we should wait longer than 0/1ms.
         // We don't know how long execution of quite complex getClipboardData will take
         // and in for example 'paste' listner execCommand() (which fires 'paste') is called
         // after getClipboardData finishes.
         // Luckily, it's impossible to immediately fire another 'paste' event we want to handle,
         // because we only handle there native context menu and menu bar.
         setTimeout( function() {
            preventPasteEvent = 0;
         }, 100 );
      }

      function preventBeforePasteEventNow() {
         preventBeforePasteEvent = 1;
         setTimeout( function() {
            preventBeforePasteEvent = 0;
         }, 10 );
      }

      // Tries to execute any of the paste, cut or copy commands in IE. Returns a
      // boolean indicating that the operation succeeded.
      // @param {String} command *LOWER CASED* name of command ('paste', 'cut', 'copy').
      function execIECommand( command ) {
         var doc = editor.document,
            body = doc.getBody(),
            enabled = false,
            onExec = function() {
               enabled = true;
            };

         // The following seems to be the only reliable way to detect that
         // clipboard commands are enabled in IE. It will fire the
         // onpaste/oncut/oncopy events only if the security settings allowed
         // the command to execute.
         body.on( command, onExec );

         // IE6/7: document.execCommand has problem to paste into positioned element.
         ( VED.env.version > 7 ? doc.$ : doc.$.selection.createRange() )[ 'execCommand' ]( command );

         body.removeListener( command, onExec );

         return enabled;
      }

      function firePasteEvents( type, data, withBeforePaste ) {
         var eventData = { type: type };

         if ( withBeforePaste ) {
            // Fire 'beforePaste' event so clipboard flavor get customized
            // by other plugins.
            if ( !editor.fire( 'beforePaste', eventData ) )
               return false; // Event canceled
         }

         // The very last guard to make sure the paste has successfully happened.
         // This check should be done after firing 'beforePaste' because for native paste
         // 'beforePaste' is by default fired even for empty clipboard.
         if ( !data )
            return false;

         // Reuse eventData.type because the default one could be changed by beforePaste listeners.
         eventData.dataValue = data;

         return editor.fire( 'paste', eventData );
      }

      // Cutting off control type element in IE standards breaks the selection entirely. (#4881)
      function fixCut() {
         if ( !VED.env.ie || VED.env.quirks )
            return;

         var sel = editor.getSelection(),
            control, range, dummy;

         if ( ( sel.getType() == VED.SELECTION_ELEMENT ) && ( control = sel.getSelectedElement() ) ) {
            range = sel.getRanges()[ 0 ];
            dummy = editor.document.createText( '' );
            dummy.insertBefore( control );
            range.setStartBefore( dummy );
            range.setEndAfter( control );
            sel.selectRanges( [ range ] );

            // Clear up the fix if the paste wasn't succeeded.
            setTimeout( function() {
               // Element still online?
               if ( control.getParent() ) {
                  dummy.remove();
                  sel.selectElement( control );
               }
            }, 0 );
         }
      }

      // Allow to peek clipboard content by redirecting the
      // pasting content into a temporary bin and grab the content of it.
      function getClipboardDataByPastebin( evt, callback ) {
         var doc = editor.document,
            editable = editor.editable(),
            cancel = function( evt ) {
               evt.cancel();
            },
            ff3x = VED.env.gecko && VED.env.version <= 10902;

         // Avoid recursions on 'paste' event or consequent paste too fast. (#5730)
         if ( doc.getById( 'ved_pastebin' ) )
            return;

         var sel = editor.getSelection();
         var bms = sel.createBookmarks();

         // Create container to paste into.
         // For rich content we prefer to use "body" since it holds
         // the least possibility to be splitted by pasted content, while this may
         // breaks the text selection on a frame-less editable, "div" would be
         // the best one in that case.
         // In another case on old IEs moving the selection into a "body" paste bin causes error panic.
         // Body can't be also used for Opera which fills it with <br>
         // what is indistinguishable from pasted <br> (copying <br> in Opera isn't possible,
         // but it can be copied from other browser).
         var pastebin = new VED.dom.element(
            editable.is( 'body' ) && !( VED.env.ie || VED.env.opera ) ? 'body' : 'div', doc );

         pastebin.setAttribute( 'id', 'ved_pastebin' );

         // Append bogus to prevent Opera from doing this. (#9522)
         if ( VED.env.opera )
            pastebin.appendBogus();

         var containerOffset = 0,
            win = doc.getWindow();

         // Seems to be the only way to avoid page scroll in Fx 3.x.
         if ( ff3x ) {
            pastebin.insertAfter( bms[ 0 ].startNode );
            pastebin.setStyle( 'display', 'inline' );
         } else {
            if ( VED.env.webkit ) {
               // It's better to paste close to the real paste destination, so inherited styles
               // (which Webkits will try to compensate by styling span) differs less from the destination's one.
               editable.append( pastebin );
               // Style pastebin like .ved_editable, to minimize differences between origin and destination. (#9754)
               pastebin.addClass( 'ved_editable' );
               // Compensate position of offsetParent.
               containerOffset = ( editable.is( 'body' ) ? editable : VED.dom.element.get( pastebin.$.offsetParent ) ).getDocumentPosition().y;
            } else {
               // Opera and IE doesn't allow to append to html element.
               editable.getAscendant( VED.env.ie || VED.env.opera ? 'body' : 'html', 1 ).append( pastebin );
            }

            pastebin.setStyles({
               position: 'absolute',
               // Position the bin at the top (+10 for safety) of viewport to avoid any subsequent document scroll.
               top: ( win.getScrollPosition().y - containerOffset + 10 ) + 'px',
               width: '1px',
               // Caret has to fit in that height, otherwise browsers like Chrome & Opera will scroll window to show it.
               // Set height equal to viewport's height - 20px (safety gaps), minimum 1px.
               height: Math.max( 1, win.getViewPaneSize().height - 20 ) + 'px',
               overflow: 'hidden',
               // Reset styles that can mess up pastebin position.
               margin: 0,
               padding: 0
            });
         }

         // Check if the paste bin now establishes new editing host.
         var isEditingHost = pastebin.getParent().isReadOnly();

         if ( isEditingHost ) {
            // Hide the paste bin.
            pastebin.setOpacity( 0 );
            // And make it editable.
            pastebin.setAttribute( 'contenteditable', true );
         }
         // Transparency is not enough since positioned non-editing host always shows
         // resize handler, pull it off the screen instead.
         else
            pastebin.setStyle( editor.config.contentsLangDirection == 'ltr' ? 'left' : 'right', '-1000px' );

         editor.on( 'selectionChange', cancel, null, null, 0 );

         // Temporarily move selection to the pastebin.
         isEditingHost && pastebin.focus();
         var range = new VED.dom.range( pastebin );
         range.selectNodeContents( pastebin );
         var selPastebin = range.select();

         // If non-native paste is executed, IE will open security alert and blur editable.
         // Editable will then lock selection inside itself and after accepting security alert
         // this selection will be restored. We overwrite stored selection, so it's restored
         // in pastebin. (#9552)
         if ( VED.env.ie ) {
            var blurListener = editable.once( 'blur', function( evt ) {
               editor.lockSelection( selPastebin );
            } );
         }

         var scrollTop = VED.document.getWindow().getScrollPosition().y;

         // Wait a while and grab the pasted contents.
         setTimeout( function() {
            // Restore main window's scroll position which could have been changed
            // by browser in cases described in #9771.
            if ( VED.env.webkit || VED.env.opera )
               VED.document[ VED.env.webkit ? 'getBody' : 'getDocumentElement' ]().$.scrollTop = scrollTop;

            // Blur will be fired only on non-native paste. In other case manually remove listener.
            blurListener && blurListener.removeListener();

            // Restore properly the document focus. (#8849)
            if ( VED.env.ie )
               editable.focus();

            // IE7: selection must go before removing pastebin. (#8691)
            sel.selectBookmarks( bms );
            pastebin.remove();

            // Grab the HTML contents.
            // We need to look for a apple style wrapper on webkit it also adds
            // a div wrapper if you copy/paste the body of the editor.
            // Remove hidden div and restore selection.
            var bogusSpan;
            if ( VED.env.webkit && ( bogusSpan = pastebin.getFirst() ) && ( bogusSpan.is && bogusSpan.hasClass( 'Apple-style-span' ) ) )
               pastebin = bogusSpan;

            editor.removeListener( 'selectionChange', cancel );
            callback( pastebin.getHtml() );
         }, 0 );
      }

      // Try to get content directly from clipboard, without native event
      // being fired before. In other words - synthetically get clipboard data
      // if it's possible.
      // mainPasteEvent will be fired, so if forced native paste:
      // * worked, getClipboardDataByPastebin will grab it,
      // * didn't work, pastebin will be empty and editor#paste won't be fired.
      function getClipboardDataDirectly() {
         if ( VED.env.ie ) {
            // Prevent IE from pasting at the begining of the document.
            editor.focus();

            // Command will be handled by 'beforepaste', but as
            // execIECommand( 'paste' ) will fire also 'paste' event
            // we're canceling it.
            preventPasteEventNow();

            // #9247: Lock focus to prevent IE from hiding toolbar for inline editor.
            var focusManager = editor.focusManager;
            focusManager.lock();

            if ( editor.editable().fire( mainPasteEvent ) && !execIECommand( 'paste' ) ) {
               focusManager.unlock();
               return false;
            }
            focusManager.unlock();
         } else {
            try {
               if ( editor.editable().fire( mainPasteEvent ) && !editor.document.$.execCommand( 'Paste', false, null ) ) {
                  throw 0;
               }
            } catch ( e ) {
               return false;
            }
         }

         return true;
      }

      // Listens for some clipboard related keystrokes, so they get customized.
      // Needs to be bind to keydown event.
      function onKey( event ) {
         if ( editor.mode != 'wysiwyg' )
            return;

         switch ( event.data.keyCode ) {
            // Paste
            case VED.CTRL + 86: // CTRL+V
            case VED.SHIFT + 45: // SHIFT+INS
               var editable = editor.editable();

               // Cancel 'paste' event because ctrl+v is for IE handled
               // by 'beforepaste'.
               preventPasteEventNow();

               // Simulate 'beforepaste' event for all none-IEs.
               !VED.env.ie && editable.fire( 'beforepaste' );

               // Simulate 'paste' event for Opera/Firefox2.
               if ( VED.env.opera || VED.env.gecko && VED.env.version < 10900 )
                  editable.fire( 'paste' );
               return;

               // Cut
            case VED.CTRL + 88: // CTRL+X
            case VED.SHIFT + 46: // SHIFT+DEL
               // Save Undo snapshot.
               editor.fire( 'saveSnapshot' ); // Save before cut
               setTimeout( function() {
                  editor.fire( 'saveSnapshot' ); // Save after cut
               }, 0 );
         }
      }

      function pasteDataFromClipboard( evt ) {
         // Default type is 'auto', but can be changed by beforePaste listeners.
         var eventData = { type: 'auto' };
         // Fire 'beforePaste' event so clipboard flavor get customized by other plugins.
         // If 'beforePaste' is canceled continue executing getClipboardDataByPastebin and then do nothing
         // (do not fire 'paste', 'afterPaste' events). This way we can grab all - synthetically
         // and natively pasted content and prevent its insertion into editor
         // after canceling 'beforePaste' event.
         var beforePasteNotCanceled = editor.fire( 'beforePaste', eventData );

         getClipboardDataByPastebin( evt, function( data ) {
            // Clean up.
            data = data.replace( /<span[^>]+data-ved-bookmark[^<]*?<\/span>/ig, '' );

            // Fire remaining events (without beforePaste)
            beforePasteNotCanceled && firePasteEvents( eventData.type, data, 0, 1 );
         });
      }

      function setToolbarStates() {
         if ( !editor.mode || editor.mode != 'wysiwyg' )
            return;

         var pasteState = stateFromNamedCommand( 'Paste' );

         editor.getCommand( 'cut' ).setState( stateFromNamedCommand( 'Cut' ) );
         editor.getCommand( 'copy' ).setState( stateFromNamedCommand( 'Copy' ) );
         editor.getCommand( 'paste' ).setState( pasteState );
         editor.fire( 'pasteState', pasteState );
      }

      function stateFromNamedCommand( command ) {
         var retval;

         if ( inReadOnly && command in { Paste:1,Cut:1 } )
            return VED.TRISTATE_DISABLED;

         if ( command == 'Paste' ) {
            // IE Bug: queryCommandEnabled('paste') fires also 'beforepaste(copy/cut)',
            // guard to distinguish from the ordinary sources (either
            // keyboard paste or execCommand) (#4874).
            VED.env.ie && ( preventBeforePasteEvent = 1 );
            try {
               // Always return true for Webkit (which always returns false)
               retval = editor.document.$.queryCommandEnabled( command ) || VED.env.webkit;
            } catch ( er ) {}
            preventBeforePasteEvent = 0;
         }
         // Cut, Copy - check if the selection is not empty
         else {
            var sel = editor.getSelection(),
               ranges = sel.getRanges();
            retval = sel.getType() != VED.SELECTION_NONE && !( ranges.length == 1 && ranges[ 0 ].collapsed );
         }

         return retval ? VED.TRISTATE_OFF : VED.TRISTATE_DISABLED;
      }
   }

   // Returns:
   // * 'htmlifiedtext' if content looks like transformed by browser from plain text.
   //      See clipboard/paste.html TCs for more info.
   // * 'html' if it is not 'htmlifiedtext'.
   function recogniseContentType( data ) {
      if ( VED.env.webkit ) {
         // Plain text or ( <div><br></div> and text inside <div> ).
         if ( !data.match( /^[^<]*$/g ) && !data.match( /^(<div><br( ?\/)?><\/div>|<div>[^<]*<\/div>)*$/gi ) )
            return 'html';
      } else if ( VED.env.ie ) {
         // Text and <br> or ( text and <br> in <p> - paragraphs can be separated by new \r\n ).
         if ( !data.match( /^([^<]|<br( ?\/)?>)*$/gi ) && !data.match( /^(<p>([^<]|<br( ?\/)?>)*<\/p>|(\r\n))*$/gi ) )
            return 'html';
      } else if ( VED.env.gecko || VED.env.opera ) {
         // Text or <br>.
         if ( !data.match( /^([^<]|<br( ?\/)?>)*$/gi ) )
            return 'html';
      } else
         return 'html';

      return 'htmlifiedtext';
   }

   // This function transforms what browsers produce when
   // pasting plain text into editable element (see clipboard/paste.html TCs
   // for more info) into correct HTML (similar to that produced by text2Html).
   function htmlifiedTextHtmlification( config, data ) {
      function repeatParagraphs( repeats ) {
         // Repeat blocks floor((n+1)/2) times.
         // Even number of repeats - add <br> at the beginning of last <p>.
         return VED.tools.repeat( '</p><p>', ~~ ( repeats / 2 ) ) + ( repeats % 2 == 1 ? '<br>' : '' );
      }

         // Replace adjacent white-spaces (EOLs too - Fx sometimes keeps them) with one space.
      data = data.replace( /\s+/g, ' ' )
         // Remove spaces from between tags.
         .replace( /> +</g, '><' )
         // Normalize XHTML syntax and upper cased <br> tags.
         .replace( /<br ?\/>/gi, '<br>' );

      // IE - lower cased tags.
      data = data.replace( /<\/?[A-Z]+>/g, function( match ) {
         return match.toLowerCase();
      });

      // Don't touch single lines (no <br|p|div>) - nothing to do here.
      if ( data.match( /^[^<]$/ ) )
         return data;

      // Webkit.
      if ( VED.env.webkit && data.indexOf( '<div>' ) > -1 ) {
            // One line break at the beginning - insert <br>
         data = data.replace( /^(<div>(<br>|)<\/div>)(?!$|(<div>(<br>|)<\/div>))/g, '<br>' )
            // Two or more - reduce number of new lines by one.
            .replace( /^(<div>(<br>|)<\/div>){2}(?!$)/g, '<div></div>' );

         // Two line breaks create one paragraph in Webkit.
         if ( data.match( /<div>(<br>|)<\/div>/ ) ) {
            data = '<p>' + data.replace( /(<div>(<br>|)<\/div>)+/g, function( match ) {
               return repeatParagraphs( match.split( '</div><div>' ).length + 1 );
            }) + '</p>';
         }

         // One line break create br.
         data = data.replace( /<\/div><div>/g, '<br>' );

         // Remove remaining divs.
         data = data.replace( /<\/?div>/g, '' );
      }

      // Opera and Firefox and enterMode != BR.
      if ( ( VED.env.gecko || VED.env.opera ) && config.enterMode != VED.ENTER_BR ) {
         // Remove bogus <br> - Fx generates two <brs> for one line break.
         // For two line breaks it still produces two <brs>, but it's better to ignore this case than the first one.
         if ( VED.env.gecko )
            data = data.replace( /^<br><br>$/, '<br>' );

         // This line satisfy edge case when for Opera we have two line breaks
         //data = data.replace( /)

         if ( data.indexOf( '<br><br>' ) > -1 ) {
            // Two line breaks create one paragraph, three - 2, four - 3, etc.
            data = '<p>' + data.replace( /(<br>){2,}/g, function( match ) {
               return repeatParagraphs( match.length / 4 );
            }) + '</p>';
         }
      }

      return switchEnterMode( config, data );
   }

   // Filter can be editor dependent.
   function getTextificationFilter( editor ) {
      var filter = new VED.htmlParser.filter();

      // Elements which creates vertical breaks (have vert margins) - took from HTML5 spec.
      // http://dev.w3.org/html5/markup/Overview.html#toc
      var replaceWithParaIf = { blockquote:1,dl:1,fieldset:1,h1:1,h2:1,h3:1,h4:1,h5:1,h6:1,ol:1,p:1,table:1,ul:1 },

         // All names except of <br>.
         stripInlineIf = VED.tools.extend({ br: 0 }, VED.dtd.$inline ),

         // What's finally allowed (ved:br will be removed later).
         allowedIf = { p:1,br:1,'ved:br':1 },

         knownIf = VED.dtd,

         // All names that will be removed (with content).
         removeIf = VED.tools.extend( { area:1,basefont:1,embed:1,iframe:1,map:1,object:1,param:1 }, VED.dtd.$nonBodyContent, VED.dtd.$cdata );

      var flattenTableCell = function( element ) {
            delete element.name;
            element.add( new VED.htmlParser.text( ' ' ) );
         },
         // Squash adjacent headers into one. <h1>A</h1><h2>B</h2> -> <h1>A<br>B</h1><h2></h2>
         // Empty ones will be removed later.
         squashHeader = function( element ) {
            var next = element,
               br, el;

            while ( ( next = next.next ) && next.name && next.name.match( /^h\d$/ ) ) {
               // TODO shitty code - waitin' for htmlParse.element fix.
               br = new VED.htmlParser.element( 'ved:br' );
               br.isEmpty = true;
               element.add( br );
               while ( ( el = next.children.shift() ) )
                  element.add( el );
            }
         };

      filter.addRules({
         elements: {
            h1: squashHeader,
            h2: squashHeader,
            h3: squashHeader,
            h4: squashHeader,
            h5: squashHeader,
            h6: squashHeader,

            img: function( element ) {
               var alt = ( element.attributes.alt || '' ).trim(),
                  txt = ' ';

               // Replace image with its alt if it doesn't look like an url or is empty.
               if ( alt && !alt.match( /(^http|\.(jpe?g|gif|png))/i ) )
                  txt = ' [' + alt + '] ';

               return new VED.htmlParser.text( txt );
            },

            td: flattenTableCell,
            th: flattenTableCell,

            $: function( element ) {
               var initialName = element.name,
                  br;

               // Remove entirely.
               if ( removeIf[ initialName ] )
                  return false;

               // Remove all attributes.
               delete element.attributes;

               // Pass brs.
               if ( initialName == 'br' )
                  return element;

               // Elements that we want to replace with paragraphs.
               if ( replaceWithParaIf[ initialName ] )
                  element.name = 'p';

               // Elements that we want to strip (tags only, without the content).
               else if ( stripInlineIf[ initialName ] )
                  delete element.name;

               // Surround other known element with <brs> and strip tags.
               else if ( knownIf[ initialName ] ) {
                  // TODO shitty code - waitin' for htmlParse.element fix.
                  br = new VED.htmlParser.element( 'ved:br' );
                  br.isEmpty = true;

                  // Replace hrs (maybe sth else too?) with only one br.
                  if ( VED.dtd.$empty[ initialName ] )
                     return br;

                  element.add( br, 0 );
                  br = br.clone();
                  br.isEmpty = true;
                  element.add( br );
                  delete element.name;
               }

               // Final cleanup - if we can still find some not allowed elements then strip their names.
               if ( !allowedIf[ element.name ] )
                  delete element.name;

               return element;
            }
         }
      });

      return filter;
   }

   function htmlTextification( config, data, filter ) {
      var fragment = new VED.htmlParser.fragment.fromHtml( data ),
         writer = new VED.htmlParser.basicWriter();

      fragment.writeHtml( writer, filter );
      data = writer.getHtml();

      // Cleanup ved:brs.
      data = data.replace( /\s*(<\/?[a-z:]+ ?\/?>)\s*/g, '$1' )   // Remove spaces around tags.
         .replace( /(<ved:br \/>){2,}/g, '<ved:br />' )         // Join multiple adjacent ved:brs
         .replace( /(<ved:br \/>)(<\/?p>|<br \/>)/g, '$2' )      // Strip ved:brs adjacent to original brs or ps.
         .replace( /(<\/?p>|<br \/>)(<ved:br \/>)/g, '$1' )
         .replace( /<(ved:)?br( \/)?>/g, '<br>' )            // Finally - rename ved:brs to brs and fix <br /> to <br>.
         .replace( /<p><\/p>/g, '' );                     // Remove empty paragraphs.

      // Fix nested ps. E.g.:
      // <p>A<p>B<p>C</p>D<p>E</p>F</p>G
      // <p>A</p><p>B</p><p>C</p><p>D</p><p>E</p><p>F</p>G
      var nested = 0;
      data = data.replace( /<\/?p>/g, function( match ) {
         if ( match == '<p>' ) {
            if ( ++nested > 1 )
               return '</p><p>';
         } else {
            if ( --nested > 0 )
               return '</p><p>';
         }

         return match;
      }).replace( /<p><\/p>/g, '' ); // Step before: </p></p> -> </p><p></p><p>. Fix this here.

      return switchEnterMode( config, data );
   }

   function switchEnterMode( config, data ) {
      if ( config.enterMode == VED.ENTER_BR ) {
         data = data.replace( /(<\/p><p>)+/g, function( match ) {
            return VED.tools.repeat( '<br>', match.length / 7 * 2 );
         }).replace( /<\/?p>/g, '' );
      } else if ( config.enterMode == VED.ENTER_DIV ) {
         data = data.replace( /<(\/)?p>/g, '<$1div>' );
      }

      return data;
   }

   })();





